<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Spark • Splat Flow</title>
    <style>
      html, body { margin: 0; height: 100%; width: 100%; background-color: black; }
      #canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; outline: none; touch-action: none; }
    </style>
  </head>
  <body>
    <canvas id="canvas" tabindex="0"></canvas>
    <script type="importmap">
      {
        "imports": {
          "three": "/examples/js/vendor/three/build/three.module.js",
          "lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
          "@sparkjsdev/spark": "/dist/spark.module.js"
        }
      }
    </script>
    <script type="module">
      import { dyno, SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
      import * as THREE from "three";
      import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
      import { getAssetFileURL } from "/examples/js/get-asset-url.js";
      import { GUI } from "lil-gui";

      const splatFiles = ["woobles.spz", "dessert.spz", "robot-head.spz"];
      const skyFile = "dali-env.glb";
      const PARAMETERS = { speedMultiplier: 1.0, objectRotation: true, pause: false, fixedMinScale: false, waves: 0.5, cameraRotation: true };
      const PAUSE_SECONDS = 2.0;

      function getTransitionState(t, fadeInTime, fadeOutTime, period) {
        const one = dyno.dynoFloat(1.0);
        const pauseTime = dyno.dynoFloat(PAUSE_SECONDS);
        const cycleTime = dyno.add(one, pauseTime);
        const total = dyno.mul(period, cycleTime);
        const wrapT = dyno.mod(t, total);
        const pos = dyno.mod(wrapT, cycleTime);
        const inPause = dyno.greaterThan(pos, one);
        const normT = dyno.select(inPause, one, pos);
        const fadeIn = dyno.and(
          dyno.greaterThan(wrapT, dyno.mul(fadeInTime, cycleTime)),
          dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeInTime, one), cycleTime))
        );
        const fadeOut = dyno.and(
          dyno.greaterThan(wrapT, dyno.mul(fadeOutTime, cycleTime)),
          dyno.lessThan(wrapT, dyno.mul(dyno.add(fadeOutTime, one), cycleTime))
        );
        return { inTransition: dyno.or(fadeIn, fadeOut), isFadeIn: fadeIn, normT };
      }

      function contractionDyno(centerGLSL) {
        return new dyno.Dyno({
          inTypes: { gsplat: dyno.Gsplat, inTransition: "bool", fadeIn: "bool", t: "float", gt: "float", objectIndex: "int", fixedMinScale: "bool", waves: "float" },
          outTypes: { gsplat: dyno.Gsplat },
          globals: () => [ dyno.unindent(`
            float hash13(vec3 p3) { p3 = fract(p3 * .1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
            float hash11(float p) { p = fract(p * .1031); p += dot(p, p + 33.33); return fract(p * p); }
            float fadeInOut(float t) { return abs(mix(-1., 1., t)); }
            ${centerGLSL}
            float applyBrightness(float t) { return .5 + fadeInOut(t) * .5; }
            vec3 applyCenter(vec3 center, float t, float id, int idx, float waves) {
              int next = (idx + 1) % 3;
              vec3 cNext = getCenterOfMass(next);
              vec3 cOwn = getCenterOfMass(idx);
              float f = fadeInOut(t);
              float v = .5 + hash11(id) * 2.;
              vec3 p = t < .5 ? mix(cNext, center, pow(f, v)) : mix(cOwn, center, pow(f, v));
              return p + length(sin(p*2.5)) * waves * (1.-f)*smoothstep(0.5,0.,t) * 2.;
            }
            vec3 applyScale(vec3 s, float t, bool fixedMin) { return mix(fixedMin ? vec3(.02) : s * .2, s, pow(fadeInOut(t), 3.)); }
            float applyOpacity(float t, float gt, int idx) {
              float p = float(${PAUSE_SECONDS});
              float c = 1.0 + p;
              float tot = 3.0 * c;
              float w = mod(gt + p + .5, tot);
              int cur = int(floor(w / c));
              return cur == idx ? .1+fadeInOut(t) : 0.0;
            }
          `) ],
          statements: ({ inputs, outputs }) => dyno.unindentLines(`
            ${outputs.gsplat} = ${inputs.gsplat};
            ${outputs.gsplat}.center = applyCenter(${inputs.gsplat}.center, ${inputs.t}, float(${inputs.gsplat}.index), ${inputs.objectIndex}, ${inputs.waves});
            ${outputs.gsplat}.scales = applyScale(${inputs.gsplat}.scales, ${inputs.t}, ${inputs.fixedMinScale});
            ${outputs.gsplat}.rgba.a *= applyOpacity(${inputs.t}, ${inputs.gt}, ${inputs.objectIndex});
            ${outputs.gsplat}.rgba.rgb *= applyBrightness(${inputs.t});
          `)
        });
      }

      function getTransitionModifier(inTrans, fadeIn, t, idx, gt, centerGLSL, fixedMinScale, waves) {
        const dyn = contractionDyno(centerGLSL);
        return dyno.dynoBlock(
          { gsplat: dyno.Gsplat },
          { gsplat: dyno.Gsplat },
          ({ gsplat }) => ({ gsplat: dyn.apply({ gsplat, inTransition: inTrans, fadeIn, t, gt, objectIndex: idx, fixedMinScale, waves }).gsplat })
        );
      }

      async function loadGLB(file, isEnv = false) {
        const url = await getAssetFileURL(file);
        const loader = new GLTFLoader();
        const gltf = await new Promise((res, rej) => loader.load(url, res, undefined, rej));
        gltf.scene.traverse(child => {
          if (child.isMesh && child.material) {
            const mat = new THREE.MeshBasicMaterial({ color: child.material.color, map: child.material.map });
            if (isEnv) {
              mat.side = THREE.BackSide;
              mat.map.mapping = THREE.EquirectangularReflectionMapping;
              mat.map.colorSpace = THREE.LinearSRGBColorSpace;
              mat.map.needsUpdate = true;
            }
            child.material = mat;
          }
        });
        return gltf.scene;
      }

      const canvas = document.getElementById("canvas");
      const renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
      renderer.setClearColor(0x000000, 1);

      const scene = new THREE.Scene();
      const spark = new SparkRenderer({ renderer });
      scene.add(spark);

      const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.01, 1000);
      camera.position.set(0, 5, 8);
      camera.lookAt(0, 0, 0);
      scene.add(camera);

      window.addEventListener("resize", () => {
        const w = canvas.clientWidth;
        const h = canvas.clientHeight;
        renderer.setSize(w, h, false);
        camera.aspect = w / h;
        camera.updateProjectionMatrix();
      });

      const time = dyno.dynoFloat(0.0);

      async function loadAssets() {
        const env = await loadGLB(skyFile, true);
        scene.add(env);

        const meshes = [];
        const period = dyno.dynoFloat(splatFiles.length);
        const positions = [
          new THREE.Vector3(-5, -2.2, -3),
          new THREE.Vector3(5, -2.5, 0),
          new THREE.Vector3(0, 1.5, 2)
        ];

        for (let i = 0; i < splatFiles.length; i++) {
          const url = await getAssetFileURL(splatFiles[i]);
          const m = new SplatMesh({ url });
          await m.initialized;
          m.position.copy(positions[i]);
          m.rotateX(Math.PI);
          scene.add(m);
          meshes.push(m);
        }

        const centers = meshes.map(m => {
          const box = new THREE.Box3();
          m.packedSplats.forEachSplat((_, c) => {
            box.expandByPoint(c);
          });
          const localCenter = box.getCenter(new THREE.Vector3());
          localCenter.y = -localCenter.y;
          return localCenter.add(m.position);
        });

        const centerGLSL = `
          vec3 getCenterOfMass(int idx) {
            if (idx == 0) return vec3(${centers[0].x}, ${centers[0].y}, ${centers[0].z});
            if (idx == 1) return vec3(${centers[1].x}, ${centers[1].y}, ${centers[1].z});
            if (idx == 2) return vec3(${centers[2].x}, ${centers[2].y}, ${centers[2].z});
            return vec3(0.0);
          }
        `;

        meshes.forEach((m, i) => {
          const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), period);
          m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
          m.updateGenerator();
        });

        return { splatMeshes: meshes, centerGLSL };
      }

      const { splatMeshes, centerGLSL } = await loadAssets();
      const gui = new GUI();
      gui.add(PARAMETERS, "speedMultiplier", 0.25, 4.0, 0.01);
      gui.add(PARAMETERS, "objectRotation");
      gui.add(PARAMETERS, "pause");
      gui.add(PARAMETERS, "cameraRotation");
      gui.add(PARAMETERS, "fixedMinScale").onChange(() => {
        splatMeshes.forEach((m, i) => {
          const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
          m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
          m.updateGenerator();
        });
      });
      gui.add(PARAMETERS, "waves", 0, 1, 0.01).onChange(() => {
        splatMeshes.forEach((m, i) => {
          const { inTransition, isFadeIn, normT } = getTransitionState(time, dyno.dynoFloat(i), dyno.dynoFloat((i+1)%splatFiles.length), dyno.dynoFloat(splatFiles.length));
          m.worldModifier = getTransitionModifier(inTransition, isFadeIn, normT, dyno.dynoInt(i), time, centerGLSL, dyno.dynoBool(PARAMETERS.fixedMinScale), dyno.dynoFloat(PARAMETERS.waves));
          m.updateGenerator();
        });
      });

      let last = 0;
      renderer.setAnimationLoop(rt => {
        const t = rt * 0.0005;
        const dt = t - last;
        last = t;
        updateCamera(splatMeshes);
        renderer.render(scene, camera);
        if (!PARAMETERS.pause) {
          time.value += dt * PARAMETERS.speedMultiplier;
          if (PARAMETERS.objectRotation) splatMeshes.forEach(m => m.rotation.y += dt * PARAMETERS.speedMultiplier * 2);
        }
      });

      function updateCamera(meshes) {
        const cTime = 1 + PAUSE_SECONDS;
        const tot = meshes.length * cTime;
        const w = (time.value + PAUSE_SECONDS) % tot;
        const cur = Math.floor(w / cTime);
        const nxt = (cur + 1) % meshes.length;
        const tr = w / cTime - cur;
        const s = tr * tr * (3 - 2 * tr);
        const tgt = new THREE.Vector3().lerpVectors(meshes[cur].position, meshes[nxt].position, s**5);

        const radius = 4 + (Math.abs(s-0.5)**2)*20;
        let x, z;

        if (PARAMETERS.cameraRotation) {
          const angle = -time.value * 0.5; // Velocidad de rotación
          x = Math.cos(angle) * radius;
          z = Math.sin(angle) * radius;
        } else {
          x = 0;
          z = -radius;
        }

        const frm = tgt.clone().add(new THREE.Vector3(x, 2, z));
        camera.position.copy(frm);
        camera.lookAt(tgt);
      }
    </script>
  </body>
</html>
